MVVM in ZK 6 - Design your first MVVM page

From Documentation
DocumentationSmall Talks2011NovemberMVVM in ZK 6 - Design your first MVVM page
MVVM in ZK 6 - Design your first MVVM page

Author
Dennis Chen, Senior Engineer, Potix Corporation
Date
November 9, 2011
Version
ZK 6 FL-2012-01-11 and after

ZK 6 & MVVM

In ZK 6, we introduce a whole new data binding system called ZK Bind which is easy to use, flexible and supports MVVM development model. MVVM is an UI design pattern, similar to MVC, it represents Model, View and ViewModel. The main concept of MVVM design pattern is to separate the data and logic from the presentation.

You can read ZK Bind's introduction here. For a short to MVVM in ZK 6, please refer to this article

In this article, I will use a real case to show how you can write your first MVVM page with some basic ZK Bind syntax.

Case scenario

I will use a search example to show how you can archive MVVM design pattern in ZK 6 by using ZK Bind. Please imagine yourself creating a search page in which the searching of an item is done by a filter string; the search result of items is displayed in a list and when selecting on an item, it will also show the details of the selected item.

Design the View Model

Following the design concept of MVVM, we should design the View Model first without considering the visual effect. A View Model should not depend on a View, but consider the data and actions as its contract for interacting with the View. In this scenario, we need a String as a filter and a ListModelList<Item> for the search result and a doSearch() method to perform the search command. Furthermore, we also need a selected field to keep the item which is currently selected.

As you can see, I am defending a ViewModel and it is isolated from the View, which means that it is possible to be reused by another View and even tested by pure Java code. Following is the code of the ViewModel.

ViewModel : SearchVM.java

public class SearchVM {
	//the search condition
	String filter;
	//the search result
	ListModelList<Item> items;
	//the selected item
	Item selected;

	@Command @NotifyChange({"items","selected"})
	public void doSearch(){
		items = new ListModelList<Item>();
		items.addAll(getSearchService().search(filter));
		selected = null;
	}

	@NotifyChange
	public void setFilter(String filter) {
		this.filter = filter;
	}

	//@NotifyChange, you could ignore it too, it is triggered automatically.
	public void setSelected(Item selected) {
		this.selected = selected;
	}

	//other getter…

	protected SearchService getSearchService(){
		//search service implementation..
	}
}

SearchVM is simply a POJO, and because it is, we need to notify ZK Bind that the properties of this POJO were changed. In ZK Bind, it has the binder to help the binding between View and ViewModel. By adding @NotifyChange on a setter method[1][2] of a property, after binder sets the property, binder will be notified of the property that has been changed and that it should reload components that are bound to this changed property. You can also add @NotifyChange to a command method so that after the binder executes the method, it will be notified of the named properties that were changed by the command and that it should reload components that are bound to these changed properties. To declare a command, we have to add Java annotation @Command to a command method and the method name becomes the command name by default.

For example, by adding @NotifyChange to setFilter setter method, when binder saves user entered value with setFilter() , binder will be notified by @NotifyChange to reload any binding that is related to the filter property of the ViewModel. I also added @NotifyChange({"items","selected"}) on the doSearch() command method. This is because when doing the search per current filter value, I will create a new list of items and also reset the selected property to null; binder needs to be notified of these two property changes when the doSearch command method is called. It is also required to add @Command to doSearch() in which its command name would be "doSearch".

With @NotifyChange, it notifies the binder what and when to reload View automatically.


  1. The @NotifyChange of a property is enabled at default, therefore you can ignore it. However, if you want to change the notification target, you have to add @NotifyChange with different property names.
  2. If you want to disable the default notification of a property, you have to add @NotifyChangeDisabledon the setter method

Design a View with ZK Bind

Now that the ViewModel is ready, we are able to bind View to ViewModel, not only the data, but also the action, and the automatic reloading of changed data (thanks to @NotifyChange in SearchVM).

Below is the designed View that will be used in this article to bind with ViewModel:

Smalltalks-mvvm-in-zk6-view-example.png

Apply the BindComposer

Now, we have to create a "search.zul" and bind it to the ViewModel. To do this, set the apply attribute to org.zkoss.bind.BindComposer and bind the viewModel to "SearchVM" that was just created. BindComposer will create a binder which will read all the ZK Bind annotations and bind View and ViewModel together.

View : search.zul

<window title="Search Storage Item" border="normal" width="600px"
    apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.search.SearchVM')" >
...

</window>

The @id(name) @init(expression) here, is the syntax to assign a ViewModel to the binder, name is a string for the name of the ViewModel which will be referenced in nested bindings. expression is an EL 2.2 expression which is to be evaluated to a result value. Here are some rules about this result value:

  • If the evaluated result is a "String", it uses this "String" value as a class name to create the ViewModel
  • If the result is a "Class" object, it creates the ViewModel by Class.newInstance()
  • If the result is a non-primitive Object, then it will use it directly, other-wise, it complains with an exception.

Binding the textbox with filter data

We need a textbox to represent the filter data. When an user types in the textbox, the data is automatically saved into "vm.filter". I also want the search button to be disabled whenever the value of "vm.filter" is empty.

View : search.zul

<textbox value="@bind(vm.filter)" instant="true"/> 
<button label="Search" disabled="@load(empty vm.filter)"/>

The @bind(expression) here, is the two-way binding syntax to bind attribute of component and property of the ViewModel together. This is an unconditional binding, which means changes in a component's attribute caused by an user's action will be saved to the property of the ViewModel immediately. And any change notification of the property to the binder will cause an attribute reloading of the component immediately, too. It is also a shortcut syntax of "@load(expression) @save(expression)" without condition. @load(expression) is the one-way binding for loading a value from property of the ViewModel to the attribute of the component while @save(expression) is the one-way binding for saving a value from the attribute of the component to the property of the ViewModel.

In the above example, I have bound the vm.filter to both the value attribute of the textbox and the disabled attribute of the button. And I use @load(expression) on disabled attribute because it needs to load only value from property of the ViewModel. When editing the textbox, vm.filter will be changed and the button will be enabled or disabled immediately depending on whether vm.filter is empty or not.

Binding the listbox with search result

In ZK 6, we introduce a new feature called template. It is a perfect match when binding a collection. We will have a listbox and a template to show the search result.

View : search.zul

<listbox model="@load(vm.items)" selectedItem="@bind(vm.selected)" hflex="true" height="300px">
	<listhead>
		<listheader label="Name"/>
		<listheader label="Price" align="center" width="80px" />
		<listheader label="Quantity" align="center" width="80px" />
	</listhead>
	<template name="model" var="item">
		<listitem >
			<listcell label="@load(item.name)"/>				
			<listcell label="@load(item.price) @converter('formatedNumber', format='###,##0.00')"/>
			<listcell label="@load(item.quantity)" sclass="@bind(item.quantity lt 3 ? 'red' : '')"/>	
		</listitem>
	</template>
</listbox>

The @load(expression) can also be applied to the model attribute of the listbox component. When binding to a model, we have to provide a template, which is named "model", to render each item in the model. In the template, we have to set the name "var", so we could use it as an item in the template. We also introduce the @converter(expression) syntax, so you can write a "Converter" to convert data to attributes when loading to components, and convert back when saving to beans.

In the above example, the model of the listbox binds to vm.items while the selectedItem binds to vm.selected with a template that has responsibility to render each entry of the items of vm.items. Of course, we can also use @load() in the template, or even bind it to the sclass attribute of a listcell with a flexible expression item.quantity lt 3 ? 'red' : ''. Look into item.price. It is a double number. I want it to be shown with an expected format; therefore a built-in converter formatedNumber and a format argument of '###,##0.00' are used.

Binding the selected item

When binding the listbox, we also bind the selectedItem of the listbox to selected property of the ViewModel. You do not need to worry about selectedItem (in which its value type is a Listitem) of listbox being the incorrect data type for the model because binder will convert it to item automatically. By the binding of selectedItem, when selecting an item in the listbox, the selected property will be updated to the selected item and displayed in detail.

View : search.zul

<groupbox visible="@load(not empty vm.selected)" hflex="true" mold="3d">
	<caption label="@load(vm.selected.name)"/>
	<grid hflex="true" >
		<columns>
			<column width="120px"/>
			<column/>
		</columns>
		<rows>
			<row>Description <label value="@load(vm.selected.description)"/></row>
			<row>Price <label value="@load(vm.selected.price) @converter('formatedNumber', format='###,##0.00')"/></row>
			<row>Quantity <label value="@load(vm.selected.quantity)"  sclass="@load(vm.selected.quantity lt 3 ?'red':'')"/></row>
			<row>Total Price <label value="@load(vm.selected.totalPrice) @converter(vm.totalPriceConverter)"/></row>
		</rows>
	</grid>
</groupbox>

In the example above, we bind visible attribute of groupbox with the expression not empty vm.selected, so that the groupbox will only be visible when user selects an item. Oppositely, if no item is selected, this groupbox will not show up. The @converter() is used again here but with some differences. The converter now comes from the ViewModel . Of course, a getTotalPriceConverter method needs to be prepared in ViewModel and return a Converter.

ViewModel : SearchVM.java

public Converter getTotalPriceConverter(){
	if(totalPriceConverter!=null){
		return totalPriceConverter;
	}
	return totalPriceConverter = new Converter(){
		public Object coerceToBean(Object val, Component component,
				BindContext ctx) {
			return null;//never called in this example
		}
		public Object coerceToUi(Object val, Component component,
				BindContext ctx) {
			if(val==null) return null;
			String str = new DecimalFormat("$ ###,###,###,##0.00").format((Double)val);
			return str;
		}		
	};
}

Binding the button action to a command

Now, we have to perform a command in ViewModel when user clicks the search button. To do so, add a @command in the onClick event of the button.

<button label="Search" onClick="@command('doSearch')" disabled="@load(empty vm.filter)"/>

The @command(expression) syntax in the event of a component represents the binding of an event to a command of the ViewModel. It has some rules.

  1. The evaluation result of the expression result has to be a 'String',
  2. The string must also be the name of the command
  3. View model must have an executable method that has the annotation @Command('commandName'), if value of @Command is empty, binder use the method name as command name by default.

In the case above, onClick is binded with a doSearch command. When clicking on the button, binder will go through the command lifecycle [1] and then execute the doSearch method of the ViewModel.


  1. I talked about command lifecycle in MVVM in ZK 6 - Design CRUD page by MVVM pattern

Show case

Various View

One of the advantages of MVVM is that the ViewModel is a contract class. It holds data, action and logic. This means, users are allowed to use various Views as long as the View can be applied to the ViewModel. Here is an example of various views, we can bind combobox with vm.filter. So users can not only type the text but also select from a pre-definded filter list. As for the action, we can bind doSearch command to both onChange and onSelect events without problem.

ViewModel : SearchVM.java

<combobox value="@bind(vm.filter)" onSelect="@command('doSearch')" onChange="@command('doSearch')">
	<comboitem label="*" value="*"/>
	<comboitem label="A" value="A"/>
	<comboitem label="B" value="B"/>
	<comboitem label="C" value="C"/>
</combobox>

Test a view model

Since the ViewModel is simply a POJO, it is also possible to do unit tests on the ViewModel itself without concerning the UI elements. Here is a very straight forward test case to test SearchVM.

TestCase : SearchVMTestCase.java

public class SearchVMTestCase {
	@Test
	public void test01(){
		SearchVM vm = new SearchVM();
		
		Assert.assertNull(vm.getItems());
		vm.doSearch();
		Assert.assertNotNull(vm.getItems());
		Assert.assertEquals(20, vm.getItems().getSize());
		
		vm.setFilter("A");
		vm.doSearch();
		Assert.assertNotNull(vm.getItems());
		Assert.assertEquals(4, vm.getItems().getSize());
		
		vm.setFilter("B");
		vm.doSearch();
		Assert.assertNotNull(vm.getItems());
		Assert.assertEquals(4, vm.getItems().getSize());
		
		vm.setFilter("X");
		vm.doSearch();
		Assert.assertNotNull(vm.getItems());
		Assert.assertEquals(0, vm.getItems().getSize());
	}
}

Of course, problems will occur in preparing the testing environment, including, how to get the 'SearchService' that we are using in the example? This really depends on what container framework you use (for example, Spring, Seam or CDI), however, this is not the topic of this article.

Syntax review

ZUL annotation syntax

Syntax Explanation
viewModel="@id(name) @init(expression)" Sets the View Model
  • Has to be used with a component that has apply="org.zkoss.bind.BindComposer", if there is no such syntax, the View Model will be set to a composer
  • The 'name' is the name of the View Model
  • The 'expression' - if the evaluated value is a String, it uses this String as the class name to create a View Model instance.
  • The 'expression' - if the evaluated value is a Class, it uses this Class to create a View Model instance.
  • The 'expression' - if the evaluated value is not a primitive type, it uses it as a View Model instance directly.
comp-attribute="@load(expression)" One-way binding to load property of an expression to a component's attribute.
  • Users can specify binding condition in expression with arugment before and after, i.e. @load(vm.filter, after='myCommand'). The binder will load the property after executing the command.
comp-attribute="@save(expression)" One-way binding to save component's attribute to the property of an expression.
  • Users can specify binding condition in expression with argument before and after, i.e. @save(vm.filter, before='myCommand'). The binder will save the property before executing the command.
comp-attribute="@bind(expression)" Two-way binding between component's attribute and the property of an expression without any condition.
  • It equals @load(expression) @save(expression)
  • If the component's attribute does not support @save, binder will ignore it automatically.
  • Notify change example: for the expression 'vm.filter', if any notification say vm. or vm.'filter' (here vm means an instance) was changed, the attribute will be reloaded.
  • More complex example: for the expression 'e.f.g.h', if any notification say e.f.g.'h',e.f.'g', e.'f' or e. was changed , the attribute of the component will be reloaded.
@converter(expression, arg = arg-expression) Provide a converter for a binding
  • The 'expression' is used directly if the evaluated result is a Converter
  • The 'expression' - if the evaluated result is a literal, then get a Converter from the View Model if it has a 'getConverter(name:Stirng):Converter' method.
  • The 'expression' - if the evaluated result is a string and cannot find converter from the View Model, then get converter from ZK Bind built-in converters.
  • You can pass many arguments to Converter when doing convert. The 'arg-expression' will also be evaluated before calling to the converter method.
comp-event="@command(expression, arg =another-expression)" Event-command binding of component's event
  • The evaluated result of the expression has to be a string, while the string is also the name of the command.
  • When event is fired, it will follow the ZK Bind Lifecycle to execute the command
  • View Model has to provide a method which are annotated @Command with the command name.

Java syntax

Syntax Explanation
@NotifyChange on setter Notify the binder of a bean's property(ies) changes after it calls the setter method. It is enabled at default, therefore, you can ignore this annotation if the notification target is the same as the property.
  • If no value exists in the annotation, notify bean that the property was changed
  • If value exists in the annotation, use the value as the property or properties (if the value is a string array), notify bean that the property or properties have been changed.
@NotifyChange on command method Notify the binder of a bean's property(ies) change after it calls the command method
  • If value exists in the annotation, use the value as the property or properties (if the value is a string array), notify bean that property or properties have been changed.
@NotifyChangeDisabled on setter To disable default notification of the property
@Command('commanName') Declare a method to correspond to a command.
  • Annotation's value which is optional is a String for the command name. If it's not provided, method name is used as the command name by default.

Summary

In this article, the concept of designing a MVVM page was demonstrated, it is a good design pattern since users are allowed to design a ViewModel that is isolated from a View. Concerning only data and behaviors also make it possible to reuse this ViewModel with other Views and can even perform unit tests of this ViewModel without any View.

I also showed how ZK Bind helps you to accomplish MVVM with ZUL. With the power of ZK Bind you can easily bind ZUL to a ViewModel with well-defined annotations. There will be more upcoming articles discussing more about how MVVM works in ZK 6, before then, any feedback is always welcomed.

Downloads

[zbindexamples ] : You could download the deployable war file here, it also contains example source code of this article


Comments



Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.